package com.nulldev.util.graphics.renderIt.renderer.swing.nitrous2;

import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;

import java.awt.*;
import java.awt.image.VolatileImage;
import java.awt.peer.ComponentPeer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.reflections.ClassCache;
import com.nulldev.util.graphics.renderIt.renderer.swing.nitrous2.renderers.D3DRenderer;
import com.nulldev.util.graphics.renderIt.renderer.swing.smoothCanvas.ComponentPeerTL;

public abstract class Renderer {
	public static final int NITROUS_MIN_WIDTH = 512;
	public static final int NITROUS_MIN_HEIGHT = 512;

	/** Component that the surface is displayed on. */
	protected final ComponentPeer parent;

	/** Class of the surface's peer component. */
	protected Class<?> peerClass;

	/** Class of the surface to render on. */
	protected Class<?> surfaceClass;

	/** Cached instance of this renderer's graphics. */
	private Graphics2D surfaceGraphics;

	private VolatileImage screenBuffer;

	/** Cached instance of the screen buffer's graphics. */
	private Graphics2D bufferGraphics;

	/**
	 * Constructs a new Renderer.
	 *
	 * @param parent           Component that the surface is displayed on. (e.g. An
	 *                         instance of java.awt.Panel)
	 *
	 * @param peerClassName    Class name of the surface's peer component.
	 *
	 * @param surfaceClassName Class name of the surface to render on.
	 *
	 * @throws ClassNotFoundException If the peer or surface classes cannot be
	 *                                found. This will occur if the JRE/JDK of this
	 *                                machine doesn't support this renderer.
	 */
	public Renderer(final ComponentPeer parent, final String peerClassName, final String surfaceClassName) throws ClassNotFoundException {
		this.peerClass = ClassCache.forName(peerClassName);
		this.surfaceClass = ClassCache.forName(surfaceClassName);

		this.parent = parent;

		parent.updateGraphicsData(getGraphicsConfig());
	}

	public void blitBufferToSurface() {
		final Graphics2D surfaceGraphics = getSurfaceGraphics2D();
		if (surfaceGraphics == null)
			return;

		/*
		 * todo Specify the width/height
		 *
		 * If you consider the screen to be a JPanel, the width/height are the
		 * width/height of your JPanel.
		 *
		 * In my original implementation, I retrieved the singleton of my settings model
		 * and used the width/height from there.
		 */
		// final var settings = SettingsModel.getInstance();
		// final int width = settings.getScreenWidth();
		// final int height = settings.getScreenHeight();
		// surfaceGraphics.drawImage(this.screenBuffer, 0, 0, 512, 512, null); // todo
		// Change "512, 512" to "width, height"

		final Dimension dim = this.parent.getPreferredSize();
		final int width = dim.width;
		final int height = dim.height;
		surfaceGraphics.drawImage(this.screenBuffer, 0, 0, width, height, null);

		/*
		 * For an unknown reason, the D3D pipeline requires the graphics object to be
		 * revalidated and then for the surface to be marked as dirty.
		 *
		 * If these two actions are not performed, then nothing will be displayed.
		 */
		if (this instanceof D3DRenderer) {
			// TODO: The D3DRenderer class should handle this itself
			try {
				final Method method = SunGraphics2D.class.getDeclaredMethod("revalidateAll");
				if (!method.isAccessible())
					method.setAccessible(true);
				method.invoke(surfaceGraphics);

				((SunGraphics2D) surfaceGraphics).surfaceData.markDirty();
			} catch (NoSuchMethodException e) {
				JVM.errorln(e);
			} catch (IllegalAccessException e) {
				JVM.errorln(e);
			} catch (InvocationTargetException e) {
				JVM.errorln(e);
			}
		}
	}

	/**
	 * Attempt to create a surface and convert it to a {@link Graphics2D} object.
	 * <p/>
	 * In doing so, this method temporarily switches to the new
	 * {@link GraphicsConfiguration}. This method does not cache the newly created
	 * {@link Graphics2D} object.
	 *
	 * #method
	 *
	 * @return a {@link Graphics2D} object.
	 */
	protected Graphics2D createGraphics() {
		try {

			// Creates the surface.
			final Method createDataMethod = surfaceClass.getDeclaredMethod("createData", peerClass);
			final SurfaceData surfaceData = (SurfaceData) createDataMethod.invoke(null,
					(parent instanceof ComponentPeerTL ? ((ComponentPeerTL) parent).getTruePeer() : parent));

			// Fail if the surface is not created.
			if (surfaceData == null) {
				return null;
			}

			// Use unofficial APi to convert the surface to Graphics2D.
			return new SunGraphics2D(surfaceData, Color.BLACK, Color.BLACK, null);
		} catch (Exception e) {
			JVM.errorln(e);
			return null;
		}
	}

	/**
	 * Retrieves renderer's {@link java.awt.GraphicsConfiguration}.
	 *
	 * @return This renderer's {@link java.awt.GraphicsConfiguration}.
	 */
	protected GraphicsConfiguration getGraphicsConfig() {
		return parent.getGraphicsConfiguration();
	}

	private void applyRenderHints(final Graphics2D graphics2D) {
		if (graphics2D == null)
			return;

		// Automatically detect the best text rendering settings and apply them.
		@SuppressWarnings("unchecked")
		final Map<Object, Object> desktopHints = (Map<Object, Object>) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints");
		if (graphics2D != null && desktopHints != null)
			graphics2D.setRenderingHints(desktopHints);

		graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

		/*
		 * todo Specify the rendering hints.
		 *
		 * In my original implementation, I retrieved the singleton of my settings model
		 * and used the values from there.
		 */
		// final var settings = SettingsModel.getInstance();
		// graphics2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
		// settings.getAlphaInterpolationHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
		// settings.getAntialiasingHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
		// settings.getColorRenderingHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_DITHERING,
		// settings.getDitheringHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
		// settings.getInterpolationHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,
		// settings.getRenderingHint());
		// graphics2D.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
		// RenderingHints.VALUE_STROKE_DEFAULT);
	}

	private Graphics2D getSurfaceGraphics2D() {
		if (surfaceGraphics == null) {
			surfaceGraphics = createGraphics();
			applyRenderHints(surfaceGraphics);
		}

		if (surfaceGraphics == null)
			return surfaceGraphics;

		if (((SunGraphics2D) surfaceGraphics).surfaceData.isSurfaceLost()) {
			surfaceGraphics = createGraphics();
			applyRenderHints(surfaceGraphics);
		}

		return surfaceGraphics;
	}

	public Graphics2D getBufferGraphics2D() {
		return getBufferGraphics2D(false);
	}

	public Graphics2D getBufferGraphics2D(final boolean requireAlpha) {
		if (bufferGraphics == null) {
			final Dimension dim = this.parent.getPreferredSize();
			final int width = dim.width < NITROUS_MIN_WIDTH ? NITROUS_MIN_WIDTH : dim.width;
			final int height = dim.height < NITROUS_MIN_HEIGHT ? NITROUS_MIN_HEIGHT : dim.height;

			if (screenBuffer == null) {
				/*
				 * todo Specify the width/height
				 *
				 * If you consider the screen to be a JPanel, the width/height are the
				 * width/height of your JPanel.
				 *
				 * In my original implementation, I retrieved the singleton of my settings model
				 * and used the width/height from there.
				 */
				// final var settings = SettingsModel.getInstance();
				// final int width = settings.getScreenWidth();
				// final int height = settings.getScreenHeight();
				final int imageType = requireAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE;

				final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
				final GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
				final GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
				final ImageCapabilities ic = new ImageCapabilities(!requireAlpha);

				try {
					screenBuffer = graphicsConfiguration.createCompatibleVolatileImage(width, height, ic, imageType);
				} catch (AWTException e) {
					JVM.errorln("[Nitrous2] Failed to generate accelerated screenBuffer, falling back.");
					screenBuffer = graphicsConfiguration.createCompatibleVolatileImage(width, height, imageType);
				} // todo Change "512, 512" to "width, height"
			}

			bufferGraphics = (Graphics2D) screenBuffer.getGraphics();
			applyRenderHints(bufferGraphics);
			bufferGraphics.setClip(0, 0, width, height);
		}

		synchronized (bufferGraphics) {
			bufferGraphics.setColor(Color.BLACK);
			bufferGraphics.fillRect(0, 0, screenBuffer.getWidth(), screenBuffer.getHeight());
		}
		return bufferGraphics;
	}

	public void resetBlitImage() {
		parent.updateGraphicsData(getGraphicsConfig());
		final Graphics surfaceGraphics_old = surfaceGraphics;
		final Graphics bufferGraphics_old = bufferGraphics;
		surfaceGraphics = null;
		screenBuffer = null;
		bufferGraphics = null;
		if (surfaceGraphics_old != null)
			surfaceGraphics_old.dispose();
		if (bufferGraphics_old != null)
			bufferGraphics_old.dispose();
	}

	/**
	 * Determines if the buffer is in a bad state and needs to be re-rendered.
	 *
	 * @see VolatileImage#contentsLost()
	 *
	 * @return Whether the buffer is in a bad state and needs to be, re-rendered or
	 *         not.
	 *
	 */
	public boolean bufferContentsLost() {
		return screenBuffer.contentsLost();
	}

	public String getName() {
		return "Unknown Renderer";
	}

	public String getRendererString() {
		return "Unknown Renderer (N/A, Generic)";
	}

	/**
	 * Determines whether the specified renderer is supported on this machine.
	 *
	 * @param peerClassName    Class name of the surface's peer component.
	 *
	 * @param surfaceClassName Class name of the surface to render on.
	 *
	 * @return Whether the renderer is supported on this machine.
	 */
	public static boolean isSupported(final String peerClassName, final String surfaceClassName) {
		try {
			ClassCache.forName(peerClassName);
			ClassCache.forName(surfaceClassName);
		} catch (final ClassNotFoundException ignored) {
			return false;
		}

		return true;
	}
}